﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;

namespace Infrastructure.Listener
{
    /// <summary>
    /// File system folder queue listener.
    /// </summary>
    /// <remarks>
    /// The listener listends from the given input folder queue. if file exist in the
    /// folder the <see cref="FileReceived"/> event is raised with the received file.
    /// With the <see cref="Directory.GetCurrentDirectory"/> the client can read the file and remove
    /// it from the queue. 
    /// <div>Note: if the file is not removed it will be send again, it
    /// acts as a commit to processing the file.</div>
    /// <para>
    /// The listener listens to rooted path only, if non rooted path given it root it
    /// to <see cref="Directory"/>.
    /// </para>
    /// </remarks>
    public class FolderListener : IDisposable
    {
        #region Data members

        /// <summary>
        /// the time in milliseconds to sleep if no files found in the input path before
        /// the next check.
        /// </summary>
        private readonly int _blockingSleepTime = 1000;

        /// <summary>
        /// the time in milliseconds to wait until the listener has stoped gracefully.
        /// </summary>
        private const int STOP_TIMEOUT = 30;

        /// <summary>
        /// The directory depth to get files under the pick up folder
        /// </summary>
        private readonly int _directoryDepth;

        /// <summary>
        /// The filter used to get files from the pick up folder
        /// </summary>
        private readonly string _fileFilter;

        /// <summary>
        /// queue of files found in the input folder.
        /// <div>used for caching</div>
        /// </summary>
        private readonly Queue<string> _inputFilesQueue = new Queue<string>();

        /// <summary>
        /// the name of the listener
        /// </summary>
        private readonly string _name;

        /// <summary>
        /// The folder used to pickup files from. the input folder.
        /// </summary>
        private readonly string _pickupFolder;

        /// <summary>
        /// used for synchronization of stop and start methods
        /// </summary>
        private readonly object _startStopSyncRoot = new object();

        /// <summary>
        /// flag to indicate if the listener should execute.
        /// </summary>
        private bool _keepAlive = true;

        /// <summary>
        /// The thread that will execute the listener.
        /// </summary>
        private Thread _listenerThreads;

        readonly HashSet<string> _reportedFiles = new HashSet<string>();
        #endregion

        /// <summary>
        /// 
        /// </summary>
        /// <param name="pickupFolder"></param>
        /// <param name="filter"></param>
        /// <param name="depth"></param>
        /// <param name="blockingSleepTime"></param>
        public FolderListener ( string pickupFolder, string filter, int depth, int blockingSleepTime = 1000 )
        {
            this._blockingSleepTime = blockingSleepTime;
            _name = Path.GetDirectoryName( pickupFolder );

            // root the input path if not rooted
            _fileFilter = filter;
            _pickupFolder = Utils.ConvertToFullPath( pickupFolder );
            _directoryDepth = depth;

            // create folder if not exist
            Directory.CreateDirectory( _pickupFolder );

        }

        public bool Enabled { get { return _keepAlive; } }

        /// <summary>
        /// get or set the value to determine whether to ignore 
        /// files that already reported or no
        /// </summary>
        public bool IgnoreReportedFiles { get; set; }

        /// <summary>
        /// The folder used to pickup files from. the input folder.
        /// </summary>
        public string PickupFolder
        {
            get { return _pickupFolder; }
        }

        /// <summary>
        /// Raised when a file is received on the folder listener.
        /// </summary>
        /// <remarks>
        /// The event is raised on the listener inner thread.
        /// </remarks>
        public event Action<FolderListener, ReceivedFileInfo> FileReceived;

        /// <summary>
        /// Start listener.
        /// </summary>
        /// <exception cref="InvalidOperationException">Listener already started
        /// <div>No subscribers for FileReceived event</div></exception>
        public void StartAsync ()
        {
            lock (_startStopSyncRoot)
            {
                if (_listenerThreads != null)
                    throw new InvalidOperationException( "Listener already started" );

                if (FileReceived == null)
                    throw new InvalidOperationException( "No subscribers for FileReceived event" );

                Console.WriteLine( "Wating for files under {0}", _pickupFolder );
                _listenerThreads = new Thread( ListenerLoop );
                _listenerThreads.Name = _name;
                _listenerThreads.IsBackground = true;
                _listenerThreads.Start();
            }
        }

        /// <summary>
        /// Stop the listener.
        /// </summary>
        /// <remarks>
        /// The stop first attempts to stop gracefully for a time out time (5000msc).
        /// If the listener doesn't stop in that time abort is called.
        /// </remarks>
        public void Stop ()
        {
            lock (_startStopSyncRoot)
            {
                if (_listenerThreads != null)
                {
                    Thread[] disposeThreads = { _listenerThreads };
                    _listenerThreads = null;
                    foreach (Thread thread in disposeThreads)
                    {
                        if (thread.ThreadState != ThreadState.Stopped)
                        {
                            Console.WriteLine( "Stop listener... [{0}]", _name );
                            _keepAlive = false;
                            thread.Interrupt();
                            bool stopped = thread.Join( STOP_TIMEOUT );

                            if (!stopped)
                            {
                                Console.WriteLine( "Listener did not stop in time [{0}], call abort...", STOP_TIMEOUT );
                                thread.Abort();
                            }
                        }
                    }
                }
            }
        }

        #region IDisposable Members

        /// <summary>
        /// stop the listener.
        /// <div>same as calling stop</div>
        /// </summary>
        public void Dispose ()
        {
            Stop();
        }

        #endregion

        #region Private methods

        /// <summary>
        /// Listener working thread loop.
        /// </summary>
        private void ListenerLoop ()
        {
            try
            {
                while (_keepAlive)
                {
                    try
                    {
                        //Console.WriteLine("Try receive file...");
                        FileInfo receivedFile = TryReceive();

                        if (receivedFile != null)
                        {
                            //Console.WriteLine("File received... [{0}]", receivedFile.FullName);
                            var file = new ReceivedFileInfo( receivedFile );
                            FileReceived( this, file );

                            if (file.DeleteOnComplete && file.Exists)
                            {
                                try
                                {
                                    file.Remove();
                                }
                                catch (Exception ex)
                                {
                                    Console.WriteLine( "Failed to delete file after 'FileReceived' event [{0}]", ex );
                                }
                            }
                        }
                        else
                        {
                            Thread.Sleep( _blockingSleepTime );
                        }
                    }
                    catch (ThreadInterruptedException)
                    {
                        Console.WriteLine( "Listener interrupted... [KeepAlive: {0}]", _keepAlive );
                    }
                    catch (IOException ex)
                    {
                        Console.WriteLine( "Failed to read file [{0}]", ex.Message );
                    }
                    catch (ThreadAbortException)
                    {
                        throw;
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine( "Listener failed [{0}]", ex );
                    }
                }
            }
            catch (ThreadAbortException)
            {
                Console.WriteLine( "Listener was aborted" );
            }
        }

        /// <summary>
        /// Get the next file of the input queue.
        /// </summary>
        /// <remarks>
        /// Block until input file exist or thread is interrupted.
        /// </remarks>
        /// <returns>file info to the received file</returns>
        /// <exception cref="ThreadInterruptedException">if the thread was interrupted while
        /// waiting for input files</exception>
        private FileInfo TryReceive ()
        {
            // if the queue is empty, get input files in the input folder          
            if (_inputFilesQueue.Count == 0)
            {
                QueueInputFolderFiles();
            }

            if (_inputFilesQueue.Count == 0)
            {
                return null;
            }
       
            string receiveFilePath = _inputFilesQueue.Dequeue();

            // cope with stale files (deleted while waiting in the queue)
            while (!CheckFile( receiveFilePath ))
            {
                Console.WriteLine( "Stale queued file found, move to next in the queue..." );
                if (_inputFilesQueue.Count > 0)
                {
                    receiveFilePath = _inputFilesQueue.Dequeue();
                }
                else
                {
                    receiveFilePath = null;
                    break;
                }
            }

            // check that we found a good file
            if (receiveFilePath == null)
            {
                Console.WriteLine( "All files in the queue were stale!, retry receive..." );
                return null;
            }

            return new FileInfo( receiveFilePath );
        }

        /// <summary>
        /// Get files from input folder and enqueue them to the listener inner queue.
        /// </summary>
        /// <remarks>
        /// Block until input file exist or thread is interrupted.
        /// </remarks>
        /// files path are added to the queue, the files still exist in the file system.
        /// <exception cref="ThreadInterruptedException">if the thread was interrupted
        /// while waiting for input files</exception>
        private void QueueInputFolderFiles ()
        {
            ICollection<string> files = FileSystemUtils.GetFiles( _pickupFolder, _directoryDepth, _fileFilter, 100000 );

            while (files.Count <= 0)
            {
                Thread.Sleep( _blockingSleepTime );
                files = FileSystemUtils.GetFiles( _pickupFolder, _directoryDepth, _fileFilter, 100000 );
            }


            foreach (string file in files)
            {
                if (IgnoreReportedFiles && !_reportedFiles.Contains( file ))
                {
                    _reportedFiles.Add( file );
                    _inputFilesQueue.Enqueue( file );
                }
                else if (!IgnoreReportedFiles)
                {
                    _inputFilesQueue.Enqueue( file );
                }
            }
        }

        /// <summary>
        /// Check queued file that it exist and is not locked.
        /// </summary>
        /// <param name="receiveFilePath">file path to check</param>
        /// <returns>true-all ok, false-not exist or locked</returns>
        private static bool CheckFile ( string receiveFilePath )
        {
            if (!File.Exists( receiveFilePath ))
                return false;

            try
            {
                using (File.OpenRead( receiveFilePath ))
                {
                }
            }
            catch (Exception)
            {
                // the file is locked
                return false;
            }

            // file exist and it's not locked
            return true;
        }

        #endregion

    }
}